/*
 *    This program is free software; you can redistribute it and/or modify
 *    it under the terms of the GNU General Public License as published by
 *    the Free Software Foundation; either version 2 of the License, or
 *    (at your option) any later version.
 *
 *    This program is distributed in the hope that it will be useful,
 *    but WITHOUT ANY WARRANTY; without even the implied warranty of
 *    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *    GNU General Public License for more details.
 *
 *    You should have received a copy of the GNU General Public License
 *    along with this program; if not, write to the Free Software
 *    Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
 */

/*
 * SortedTableModel.java
 * Copyright (C) 2005-2010 University of Waikato, Hamilton, New Zealand
 *
 */

package weka.gui;

import java.awt.event.InputEvent;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.util.ArrayList;
import java.util.Collections;

import javax.swing.JTable;
import javax.swing.event.TableModelEvent;
import javax.swing.event.TableModelListener;
import javax.swing.table.AbstractTableModel;
import javax.swing.table.JTableHeader;
import javax.swing.table.TableColumnModel;
import javax.swing.table.TableModel;

import weka.core.ClassDiscovery;

/**
 * Represents a TableModel with sorting functionality.
 * 
 * @author FracPete (fracpete at waikato dot ac dot nz)
 * @version $Revision: 7059 $
 */

public class SortedTableModel extends AbstractTableModel implements
		TableModelListener {

	/** for serialization */
	static final long serialVersionUID = 4030907921461127548L;

	/**
	 * Helper class for sorting the columns.
	 */
	public static class SortContainer implements Comparable<SortContainer> {

		/** the value to sort. */
		protected Comparable m_Value;

		/** the index of the value. */
		protected int m_Index;

		/**
		 * Initializes the container.
		 * 
		 * @param value
		 *            the value to sort on
		 * @param index
		 *            the original index
		 */
		public SortContainer(Comparable value, int index) {
			super();

			m_Value = value;
			m_Index = index;
		}

		/**
		 * Returns the value to sort on.
		 * 
		 * @return the value
		 */
		public Comparable getValue() {
			return m_Value;
		}

		/**
		 * Returns the original index of the item.
		 * 
		 * @return the index
		 */
		public int getIndex() {
			return m_Index;
		}

		/**
		 * Compares this object with the specified object for order. Returns a
		 * negative integer, zero, or a positive integer as this object is less
		 * than, equal to, or greater than the specified object. Null is
		 * considered smallest. If both values are null, then 0 is returned.
		 * 
		 * @param o
		 *            the object to be compared.
		 * @return a negative integer, zero, or a positive integer as this
		 *         object is less than, equal to, or greater than the specified
		 *         object.
		 * @throws ClassCastException
		 *             if the specified object's type prevents it from being
		 *             compared to this object.
		 */
		public int compareTo(SortContainer o) {
			if ((m_Value == null) || (o.getValue() == null)) {
				if (m_Value == o.getValue())
					return 0;
				if (m_Value == null)
					return -1;
				else
					return +1;
			} else {
				return m_Value.compareTo(o.getValue());
			}
		}

		/**
		 * Indicates whether some other object is "equal to" this one.
		 * 
		 * @param obj
		 *            the reference object with which to compare.
		 * @return true if this object is the same as the obj argument; false
		 *         otherwise.
		 * @throws ClassCastException
		 *             if the specified object's type prevents it from being
		 *             compared to this object.
		 */
		public boolean equals(Object obj) {
			return (compareTo((SortContainer) obj) == 0);
		}

		/**
		 * Returns a string representation of the sort container.
		 * 
		 * @return the string representation (value + index)
		 */
		public String toString() {
			return Messages.getInstance().getString(
					"SortedTableModel_ToString_Text_First")
					+ m_Value
					+ Messages.getInstance().getString(
							"SortedTableModel_ToString_Text_Second") + m_Index;
		}
	}

	/** the actual table model */
	protected TableModel mModel;

	/** the mapping between displayed and actual index */
	protected int[] mIndices;

	/** the sort column */
	protected int mSortColumn;

	/** whether sorting is ascending or descending */
	protected boolean mAscending;

	/**
	 * initializes with no model
	 */
	public SortedTableModel() {
		this(null);
	}

	/**
	 * initializes with the given model
	 * 
	 * @param model
	 *            the model to initialize the sorted model with
	 */
	public SortedTableModel(TableModel model) {
		setModel(model);
	}

	/**
	 * sets the model to use
	 * 
	 * @param value
	 *            the model to use
	 */
	public void setModel(TableModel value) {
		mModel = value;

		// initialize indices
		if (mModel == null) {
			mIndices = null;
		} else {
			initializeIndices();
			mSortColumn = -1;
			mAscending = true;
			mModel.addTableModelListener(this);
		}
	}

	/**
	 * (re-)initializes the indices
	 */
	protected void initializeIndices() {
		int i;

		mIndices = new int[mModel.getRowCount()];
		for (i = 0; i < mIndices.length; i++)
			mIndices[i] = i;
	}

	/**
	 * returns the current model, can be null
	 * 
	 * @return the current model
	 */
	public TableModel getModel() {
		return mModel;
	}

	/**
	 * returns whether the table was sorted
	 * 
	 * @return true if the table was sorted
	 */
	public boolean isSorted() {
		return (mSortColumn > -1);
	}

	/**
	 * whether the model is initialized
	 * 
	 * @return true if the model is not null and the sort indices match the
	 *         number of rows
	 */
	protected boolean isInitialized() {
		return (getModel() != null);
	}

	/**
	 * Returns the actual underlying row the given visible one represents.
	 * Useful for retrieving "non-visual" data that is also stored in a
	 * TableModel.
	 * 
	 * @param visibleRow
	 *            the displayed row to retrieve the original row for
	 * @return the original row
	 */
	public int getActualRow(int visibleRow) {
		if (!isInitialized())
			return -1;
		else
			return mIndices[visibleRow];
	}

	/**
	 * Returns the most specific superclass for all the cell values in the
	 * column.
	 * 
	 * @param columnIndex
	 *            the index of the column
	 * @return the class of the specified column
	 */
	public Class getColumnClass(int columnIndex) {
		if (!isInitialized())
			return null;
		else
			return getModel().getColumnClass(columnIndex);
	}

	/**
	 * Returns the number of columns in the model
	 * 
	 * @return the number of columns in the model
	 */
	public int getColumnCount() {
		if (!isInitialized())
			return 0;
		else
			return getModel().getColumnCount();
	}

	/**
	 * Returns the name of the column at columnIndex
	 * 
	 * @param columnIndex
	 *            the column to retrieve the name for
	 * @return the name of the specified column
	 */
	public String getColumnName(int columnIndex) {
		if (!isInitialized())
			return null;
		else
			return getModel().getColumnName(columnIndex);
	}

	/**
	 * Returns the number of rows in the model.
	 * 
	 * @return the number of rows in the model
	 */
	public int getRowCount() {
		if (!isInitialized())
			return 0;
		else
			return getModel().getRowCount();
	}

	/**
	 * Returns the value for the cell at columnIndex and rowIndex.
	 * 
	 * @param rowIndex
	 *            the row
	 * @param columnIndex
	 *            the column
	 * @return the value of the sepcified cell
	 */
	public Object getValueAt(int rowIndex, int columnIndex) {
		if (!isInitialized())
			return null;
		else
			return getModel().getValueAt(mIndices[rowIndex], columnIndex);
	}

	/**
	 * Returns true if the cell at rowIndex and columnIndex is editable.
	 * 
	 * @param rowIndex
	 *            the row
	 * @param columnIndex
	 *            the column
	 * @return true if the cell is editable
	 */
	public boolean isCellEditable(int rowIndex, int columnIndex) {
		if (!isInitialized())
			return false;
		else
			return getModel().isCellEditable(mIndices[rowIndex], columnIndex);
	}

	/**
	 * Sets the value in the cell at columnIndex and rowIndex to aValue.
	 * 
	 * @param aValue
	 *            the new value of the cell
	 * @param rowIndex
	 *            the row
	 * @param columnIndex
	 *            the column
	 */
	public void setValueAt(Object aValue, int rowIndex, int columnIndex) {
		if (isInitialized())
			getModel().setValueAt(aValue, mIndices[rowIndex], columnIndex);
	}

	/**
	 * sorts the table over the given column (ascending)
	 * 
	 * @param columnIndex
	 *            the column to sort over
	 */
	public void sort(int columnIndex) {
		sort(columnIndex, true);
	}

	/**
	 * sorts the table over the given column, either ascending or descending
	 * 
	 * @param columnIndex
	 *            the column to sort over
	 * @param ascending
	 *            ascending if true, otherwise descending
	 */
	public void sort(int columnIndex, boolean ascending) {
		int columnType;
		int i;
		ArrayList<SortContainer> sorted;
		SortContainer cont;
		Object value;

		// can we sort?
		if ((!isInitialized()) || (getModel().getRowCount() != mIndices.length)) {

			System.out.println(this.getClass().getName()
					+ Messages.getInstance().getString(
							"SortedTableModel_Sort_Text_Firt"));

			return;
		}

		// init
		mSortColumn = columnIndex;
		mAscending = ascending;
		initializeIndices();

		// determine the column type: 0=string/other, 1=comparable
		if (ClassDiscovery.hasInterface(Comparable.class,
				getColumnClass(mSortColumn)))
			columnType = 1;
		else
			columnType = 0;

		// create list for sorting
		sorted = new ArrayList<SortContainer>();
		for (i = 0; i < getRowCount(); i++) {
			value = mModel.getValueAt(mIndices[i], mSortColumn);
			if (columnType == 0)
				cont = new SortContainer((value == null) ? null
						: value.toString(), mIndices[i]);
			else
				cont = new SortContainer((Comparable) value, mIndices[i]);
			sorted.add(cont);
		}
		Collections.sort(sorted);

		for (i = 0; i < sorted.size(); i++) {
			if (mAscending)
				mIndices[i] = sorted.get(i).getIndex();
			else
				mIndices[i] = sorted.get(sorted.size() - 1 - i).getIndex();
		}

		sorted.clear();
		sorted = null;
	}

	/**
	 * This fine grain notification tells listeners the exact range of cells,
	 * rows, or columns that changed.
	 * 
	 * @param e
	 *            the event
	 */
	public void tableChanged(TableModelEvent e) {
		initializeIndices();
		if (isSorted())
			sort(mSortColumn, mAscending);

		fireTableChanged(e);
	}

	/**
	 * Adds a mouselistener to the header: left-click on the header sorts in
	 * ascending manner, using shift-left-click in descending manner.
	 * 
	 * @param table
	 *            the table to add the listener to
	 */
	public void addMouseListenerToHeader(JTable table) {
		final SortedTableModel modelFinal = this;
		final JTable tableFinal = table;
		tableFinal.setColumnSelectionAllowed(false);
		JTableHeader header = tableFinal.getTableHeader();

		if (header != null) {
			MouseAdapter listMouseListener = new MouseAdapter() {
				public void mouseClicked(MouseEvent e) {
					TableColumnModel columnModel = tableFinal.getColumnModel();
					int viewColumn = columnModel.getColumnIndexAtX(e.getX());
					int column = tableFinal
							.convertColumnIndexToModel(viewColumn);
					if (e.getButton() == MouseEvent.BUTTON1
							&& e.getClickCount() == 1 && !e.isAltDown()
							&& column != -1) {
						int shiftPressed = e.getModifiers()
								& InputEvent.SHIFT_MASK;
						boolean ascending = (shiftPressed == 0);
						modelFinal.sort(column, ascending);
					}
				}
			};

			header.addMouseListener(listMouseListener);
		}
	}
}
